#region Copyright and Licensing Notice
// Copyright (C) 2011 Steven Bosnick
// 
// This program is open source software; you can redistribute it 
// and/or modifiy it under the terms of the Eclipse Public License,
// version 1.0.
// 
// This program is distributed in the hope that it will be useful, 
// but WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A 
// PARTICULAR PURPOSE. See the Eclipse Public License for more
// details.
// 
// You should have received a copy of the Eclipse Public License 
// along with this program; if not, you can find a copy at
// "http://www.opensource.org/licenses/eclipse-1.0.php".
#endregion
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using BitwiseOre.IO;
using BitwiseOre.SharedMemory;
using NUnit.Framework;

namespace BitwiseOre.Test.SharedMemory.Integration
{
    [TestFixture]
    public sealed class SharedMemoryListenerFixture : AssertionHelper
    {
        private SharedMemoryContext _context;

        [SetUp]
        public void SetUp()
        {
            _context = new SharedMemoryContext();
        }

        [Test(Description = "Creating with a null context throws an exception.")]
        public void Create_WithNullContext()
        {
            Expect(() => SharedMemoryListener.Create(null, "TestListenerA"),
                   Throws.InstanceOf<ArgumentNullException>(),
                   "Null context did not throw expected exception when creating a listener.");
        }

        [Test(Description = "Creating a listener with a null name throws an exception.")]
        public void Create_WithNullName()
        {
            Expect(() => SharedMemoryListener.Create(_context, null),
                Throws.InstanceOf<ArgumentNullException>(),
                "Null name did not throw expected exception when creating a listener.");
        }

        [Test(Description = "Creating a listener with a name that is in use throws an exception.")]
        public void Create_WithExistingName()
        {
            const string name = "TestListenerB";

            using (SharedMemoryListener.Create(_context, name))
            {
                Expect(() => SharedMemoryListener.Create(_context, name),
                       Throws.InstanceOf<NameAlreadyInUseException>(),
                       "Unexpected exception thrown when creating listener with a name that is in use.");
            }
        }

        [Test(Description = "Accept timesout in the expected time if there are no connection requests.")]
        public void Accept_TimesOut()
        {
            const int timeout = 1000;

            var stopwatch = new Stopwatch();
            using (var sut = SharedMemoryListener.Create(_context, "TestListenerC"))
            {
                sut.AcceptTimeout = timeout;

                stopwatch.Start();
                try
                {
                    sut.Accept();
                }
                catch (TimeoutException) {}
                stopwatch.Stop();
            }

            Expect(stopwatch.ElapsedMilliseconds, EqualTo(timeout).Within(100),
                   "Unexpected timeout time when accepting a connection.");
        }

        [Sequential,Test(Description = "Accept accepts a connection request from a factory.")]
        public void Accept_AcceptsAnIncomingConnection(
            [ValueSource("PendingConnectionsValues")] int pendingConnections)
        {
            var name = string.Format("TestListenerC{0}", pendingConnections);
            _context.ListenerContext.MaxPendingConnections = pendingConnections;

            DisconnectableStream result;
            using (var listener = SharedMemoryListener.Create(_context, name))
            using (var factory = SharedMemoryFactory.Create(_context, name))
            {
                listener.AcceptTimeout = 1000;
                factory.ConnectTimeout = 1000;

                var async = listener.BeginAccept(null, null);
                try
                {
                    factory.Connect().Dispose();
                }
                finally
                {
                    async.AsyncWaitHandle.WaitOne();
                }
                result = listener.EndAccept(async);
            }

            Expect(result, Not.Null);
        }

        [Test(Description = "Accepts accepts a connection request from a factory using all valid buffer sizes.")]
        public void Accept_AcceptsConnectionsForAllValidBufferSizes(
            [Values(4, 5, 1024, 0x10000, 0x1000000 ,0x2000000)] int bufferSize)
        {
            var name = string.Format("TestListenerC1{0}", bufferSize);
            _context.StreamContext.BufferSize = bufferSize;

            DisconnectableStream result;
            using (var listener = SharedMemoryListener.Create(_context, name))
            using (var factory = SharedMemoryFactory.Create(_context, name))
            {
                listener.AcceptTimeout = 3*60*1000;
                factory.ConnectTimeout = 3*60*1000;

                var async = factory.BeginConnect(null, null);
                result = listener.Accept();
                factory.EndConnect(async).Dispose();
                result.Dispose();
            }

            Expect(result, Not.Null);
        }


        [Test(Description = "Accept after dispose throws an exception.")]
        public void Accept_AfterDispose()
        {
            var listener = SharedMemoryListener.Create(_context, "TestListenerD");
            listener.Dispose();

            Expect(() => listener.Accept(),
                   Throws.InstanceOf<ObjectDisposedException>());
        }

        [Test(Description = "WaitFor timesout in the expected time if there are no connection request.")]
        public void WaitFor_TimesOut()
        {
            const int timeout = 1000;

            var stopwatch = new Stopwatch();
            using (var sut = SharedMemoryListener.Create(_context, "TestListenerE"))
            {
                sut.WaitForTimeout = timeout;

                stopwatch.Start();
                try
                {
                    sut.WaitFor();
                }
                catch (TimeoutException) {}
                stopwatch.Stop();
            }

            Expect(stopwatch.ElapsedMilliseconds, EqualTo(timeout).Within(50),
                   "Unexpected timeout time when accepting a connection.");
            
        }

        [Test(Description = "Waiting for a connection after disposing of the listener throws and exception.")]
        public void WaitFor_AfterDispose()
        {
            var listener = SharedMemoryListener.Create(_context, "TestListenerF");
            listener.Dispose();

            Expect(() => listener.WaitFor(),
                   Throws.InstanceOf<ObjectDisposedException>());
        }

        [Sequential,Test(Description = "WaitFor waits for a connection request from a factory.")]
        public void WaitFor_WaitsForAnIncomingCommention(
            [ValueSource("PendingConnectionsValues")] int pendingConnections
            )
        {
            var name = string.Format("TestListenerE{0}", pendingConnections);
            _context.ListenerContext.MaxPendingConnections = pendingConnections;

            using (var listener = SharedMemoryListener.Create(_context, name))
            using (var factory = SharedMemoryFactory.Create(_context, name))
            {
                listener.WaitForTimeout = 1000;
                factory.ConnectTimeout = 100;

                var async = listener.BeginWaitFor(null, null);
                try
                {
                    factory.Connect();
                }
                catch (TimeoutException)
                {
                }
                finally
                {
                    async.AsyncWaitHandle.WaitOne();
                }
                Expect(() => listener.EndWaitFor(async),
                       Throws.Nothing);
            }
        }

        [Sequential,Test(Description = "Accept after a successful waitfor accepts an incoming connection.")]
        public void Accept_AfterWaitForAcceptsAnIncomingConnection(
            [ValueSource("PendingConnectionsValues")] int pendingConnections)
        {
            var name = string.Format("TestListenerF{0}", pendingConnections);
            _context.ListenerContext.MaxPendingConnections = pendingConnections;

            DisconnectableStream result;
            using (var listener = SharedMemoryListener.Create(_context, name))
            using (var factory = SharedMemoryFactory.Create(_context, name))
            {
                listener.WaitForTimeout = 1000;
                listener.AcceptTimeout = 1000;
                factory.ConnectTimeout = 1000;

                var async = factory.BeginConnect(null, null);

                listener.WaitFor();

                result = listener.Accept();

                factory.EndConnect(async).Dispose();
                result.Dispose();
            }

            Expect(result, Not.Null);
        }

        [Sequential,Test(Description = "Accept called after a delay after a sucessful waitfor times out.")]
        public void Accept_DelayedAfterWaitForTimesout(
            [ValueSource("PendingConnectionsValues")] int pendingConnections)
        {
            var name = string.Format("TestListenerG{0}", pendingConnections);
            var connectended = new ManualResetEvent(false);

            _context.ListenerContext.MaxPendingConnections = pendingConnections;

            using (var listener = SharedMemoryListener.Create(_context, name))
            using (var factory = SharedMemoryFactory.Create(_context, name))
            {
                listener.WaitForTimeout = 1000;
                listener.AcceptTimeout = 1000;
                factory.ConnectTimeout = 100;

                var async = listener.BeginWaitFor(null, null);

                factory.BeginConnect(
                    ar =>
                        {
                            try
                            {
                                factory.EndConnect(ar);
                            }
                            catch (TimeoutException)
                            {
                            }
                            finally
                            {
                                connectended.Set();
                            }
                        }, null);

                listener.EndWaitFor(async);

                connectended.WaitOne();

                Expect(() => listener.Accept(),
                       Throws.InstanceOf<TimeoutException>());
            }

        }

        public static IEnumerable<int> PendingConnectionsValues()
        {
            var initialvalues = Enumerable.Range(1, 16);
            const int lower = 5;
            var upper = (int) Math.Truncate(Math.Log(0xffd, 2));
            var endvalues = from i in Enumerable.Range(lower, upper - lower)
                            let mid = 0x1 << i
                            from value in Enumerable.Range(mid - 1, 3)
                            select value;

            return initialvalues
                .Concat(endvalues)
                .Concat(new[] {0xffd});
        }
    }

}